package easycloud.ecs.service;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.HiddenFileFilter;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.apache.commons.lang.reflect.ConstructorUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;

import easycloud.ecs.api.IProvider;
import easycloud.ecs.config.ConfigException;
import easycloud.ecs.config.MonitorConfig;
import easycloud.ecs.config.ProviderConfig;
import easycloud.ecs.service.FileAlternationListener.ALTERNATION;

public final class MonitorService {

	private static final Log logger = LogFactory.getLog(MonitorService.class);

	private static final MonitorService instance = new MonitorService();

	public static MonitorService getInstance() {
		return MonitorService.instance;
	}

	private ExecutorService compareLocalExecutor;

	private ExecutorService compareRemoteExecutor;

	private Map<String, IProvider> providers;

	private FileAlterationMonitor localFileMonitor;

	private Multimap<File, IProvider> syncDirs;

	private boolean isRunning;

	private MonitorService() {
		compareLocalExecutor = Executors.newCachedThreadPool();
		compareRemoteExecutor = Executors.newCachedThreadPool();
		providers = new HashMap<String, IProvider>();
		localFileMonitor = new FileAlterationMonitor();
		syncDirs = HashMultimap.create();
	}

	/**
	 * Add alternated file to compare queue
	 * 
	 * @param file
	 * @param alternation
	 * @param includeDir
	 */
	public void addAlternationFile(File file, ALTERNATION alternation,
			boolean includeDir) {
		if (file.isHidden()) {
			return;
		}

		MonitorService.logger.info("add alternation file: " + file
				+ " alternation: " + alternation.toString());
		File watchedDir = getSyncDirByLocalFile(file);
		Collection<IProvider> curProviders = syncDirs.get(watchedDir);
		if ((watchedDir != null) && (curProviders != null)) {
			if (((alternation == ALTERNATION.CREATE) || (alternation == ALTERNATION.CHANGE))
					&& file.isFile()) {
				String objectName = FilenameUtils.getBaseName(watchedDir
						.getAbsolutePath())
						+ "/"
						+ file.getAbsolutePath().substring(
								watchedDir.getAbsolutePath().length() + 1);
				objectName = FilenameUtils.separatorsToUnix(objectName);
				for (IProvider provider : curProviders) {
					compareLocalExecutor.execute(new CompareLocalThread(
							provider, file, objectName, alternation));
				}
			} else if (alternation == ALTERNATION.DELETE) {
				String objectName = FilenameUtils.getBaseName(watchedDir
						.getAbsolutePath())
						+ "/"
						+ file.getAbsolutePath().substring(
								watchedDir.getAbsolutePath().length() + 1);
				objectName = FilenameUtils.separatorsToUnix(objectName);
				for (IProvider provider : curProviders) {
					compareLocalExecutor.execute(new CompareLocalThread(
							provider, file, objectName, alternation));
				}
			}
			if (includeDir && file.isDirectory()) {
				File[] files = file.listFiles();
				if (files != null) {
					for (File f : files) {
						addAlternationFile(f, alternation, true);
					}
				}
			}
		}
	}

	/**
	 * Add sync provider for the folder
	 * 
	 * @param providerName
	 * @param filePath
	 * @throws ServiceException
	 */
	public void addSyncFolder(String providerName, String filePath)
			throws ServiceException {
		IProvider provider = providers.get(providerName);
		if (provider == null) {
			MonitorService.logger.warn("Wrong provider name： " + providerName);
			throw new ServiceException("Wrong provider name： " + providerName);
		}
		if (filePath == null) {
			MonitorService.logger.warn("Only sync folder allowed： " + filePath);
			throw new ServiceException("Only sync folder allowed: " + filePath);
		}
		File dir = new File(filePath);
		if (!dir.isDirectory()) {
			MonitorService.logger.warn("Only sync folder allowed： " + filePath);
			throw new ServiceException("Only sync folder allowed: " + filePath);
		}
		MonitorService.logger.info("Add sync folder: " + dir.getAbsolutePath()
				+ " in: " + provider.getName());

		if (!syncDirs.containsKey(dir)) {
			IOFileFilter directories = FileFilterUtils.and(
					FileFilterUtils.directoryFileFilter(),
					HiddenFileFilter.VISIBLE);
			IOFileFilter files = FileFilterUtils.and(
					FileFilterUtils.fileFileFilter(), HiddenFileFilter.VISIBLE);
			IOFileFilter filter = FileFilterUtils.or(directories, files);
			FileAlterationObserver observer = new FileAlterationObserver(dir,
					filter);
			observer.addListener(new FileAlternationListener(this));
			localFileMonitor.addObserver(observer);
		}

		if (!syncDirs.containsEntry(dir, provider)) {
			syncDirs.put(dir, provider);

			if (isRunning) {
				addAlternationFile(dir, ALTERNATION.CHANGE, true);
			}
		}
	}

	public FileAlterationMonitor getFileMonitor() {
		return localFileMonitor;
	}

	public Map<String, IProvider> getProviders() {
		return providers;
	}

	/**
	 * Find sync folder by local file
	 * 
	 * @param file
	 * @return
	 */
	File getSyncDirByLocalFile(File file) {
		String curPath = file.getAbsolutePath();
		if (file.isFile()) {
			curPath = FilenameUtils.getFullPathNoEndSeparator(file
					.getAbsolutePath());
		}
		for (File syncDir : syncDirs.keys()) {
			String watchedPath = syncDir.getAbsolutePath();
			if (FilenameUtils.wildcardMatch(curPath, watchedPath + "*")) {
				return syncDir;
			}
		}
		return null;
	}

	/**
	 * Find sync folder by remote file
	 * 
	 * @param provider
	 * @param dirName
	 * @return
	 */
	File getSyncDirByRemoteFile(IProvider provider, String dirName) {
		for (Entry<File, IProvider> entry : syncDirs.entries()) {
			String watchedPath = FilenameUtils.getBaseName(entry.getKey()
					.getAbsolutePath());
			if (FilenameUtils.wildcardMatch(dirName, watchedPath + "*")) {
				if (syncDirs.containsEntry(entry.getKey(), provider)) {
					return entry.getKey();
				}
			}
		}
		return null;
	}

	public Multimap<File, IProvider> getSyncDirs() {
		return syncDirs;
	}

	/**
	 * Load monitor config
	 * 
	 * @throws IOException
	 */
	public void loadMonitorConfig(MonitorConfig monitorConfig)
			throws ConfigException {
		for (Entry<String, List<String>> entry : monitorConfig.getSyncDirs()
				.entrySet()) {
			Object propValue = entry.getValue();
			if (propValue != null) {
				List<String> providerNames = entry.getValue();
				for (String providerName : providerNames) {
					try {
						addSyncFolder(providerName, entry.getKey());
					} catch (ServiceException e) {
						MonitorService.logger.warn(
								"add Sync dir failed when load config", e);
						throw new ConfigException(
								"add Sync dir failed when load config");
					}
				}
			}
		}
	}

	/**
	 * 
	 * @param providerConfigs
	 */
	public void loadProviderConfig(List<ProviderConfig> providerConfigs)
			throws ConfigException {
		for (ProviderConfig providerConfig : providerConfigs) {
			try {
				@SuppressWarnings("rawtypes")
				Class providerClass = Class.forName(providerConfig
						.getClassName());
				IProvider provider = (IProvider) ConstructorUtils
						.invokeConstructor(providerClass, providerConfig);
				providers.put(providerConfig.getProviderName(), provider);
			} catch (ClassNotFoundException e) {
				MonitorService.logger.warn("Construct provider failed", e);
				throw new ConfigException("Construct provider failed");
			} catch (NoSuchMethodException e) {
				MonitorService.logger.warn("Construct provider failed", e);
				throw new ConfigException("Construct provider failed");
			} catch (IllegalAccessException e) {
				MonitorService.logger.warn("Construct provider failed", e);
				throw new ConfigException("Construct provider failed");
			} catch (InvocationTargetException e) {
				MonitorService.logger.warn("Construct provider failed", e);
				throw new ConfigException("Construct provider failed");
			} catch (InstantiationException e) {
				MonitorService.logger.warn("Construct provider failed", e);
				throw new ConfigException("Construct provider failed");
			}

		}
	}

	/**
	 * 
	 * @param providerName
	 * @param filePath
	 * @throws ServiceException
	 */
	public void removeSyncFolder(String providerName, String filePath)
			throws ServiceException {
		IProvider provider = providers.get(providerName);
		if (provider == null) {
			MonitorService.logger.warn("Wrong provider name： " + providerName);
			throw new ServiceException("Wrong provider name： " + providerName);
		}
		if (filePath == null) {
			MonitorService.logger.warn("Only sync folder allowed： " + filePath);
			throw new ServiceException("Only sync folder allowed: " + filePath);
		}
		File dir = new File(filePath);
		if (!dir.isDirectory()) {
			MonitorService.logger.warn("Only sync folder allowed： " + filePath);
			throw new ServiceException("Only sync folder allowed: " + filePath);
		}
		MonitorService.logger.info("Remove sync folder: "
				+ dir.getAbsolutePath() + " in: " + provider.getName());

		if (syncDirs.containsKey(dir)) {
			FileAlterationObserver curObserver = null;
			for (FileAlterationObserver observer : localFileMonitor
					.getObservers()) {
				if (observer.getDirectory().equals(dir)) {
					curObserver = observer;
					break;
				}
			}
			localFileMonitor.removeObserver(curObserver);
		}

		if (syncDirs.containsEntry(dir, provider)) {
			syncDirs.remove(dir, provider);
		}

		ETagService.getInstance().removeETags(provider, dir);
	}

	/**
	 * Start sync
	 */
	public void start() {
		if (!isRunning) {
			try {
				for (IProvider provider : syncDirs.values()) {
					compareRemoteExecutor.execute(new CompareRemoteThread(
							provider));
				}

				for (File file : syncDirs.keySet()) {
					if (file.exists() && file.isDirectory()) {
						addAlternationFile(file, ALTERNATION.CHANGE, true);
					}
				}

				localFileMonitor.start();
				isRunning = true;
			} catch (Exception e) {
				MonitorService.logger.warn("start monitor failed", e);
			}
		}
	}

	/**
	 * Stop sync
	 */
	public void stop() {
		if (isRunning) {
			try {
				compareLocalExecutor.shutdown();
				compareRemoteExecutor.shutdown();
				localFileMonitor.stop();
				isRunning = false;
			} catch (Exception e) {
				MonitorService.logger.warn("stop monitor failed", e);
			}
		}
	}

}
